package com.mrd.spring.repository.impl;

import com.mrd.exception.DeleteException;
import com.mrd.exception.QueryException;
import com.mrd.exception.SaveException;
import com.mrd.framework.ui.Jqgrid;
import com.mrd.framework.ui.filter.Filter;
import com.mrd.framework.ui.filter.SearchRule;
import com.mrd.framework.ui.filter.SearchRuleOption;
import com.mrd.spring.repository.BaseRepository;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.query.Jpa21Utils;
import org.springframework.data.jpa.repository.query.JpaEntityGraph;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;

import java.io.Serializable;
import java.math.BigDecimal;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

/**
 * 数据库操作底层实现 Created by winaya on 2016/8/8.
 */
public class BaseRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID>
        implements BaseRepository<T, ID> {
    private static final Logger logger = LoggerFactory.getLogger(BaseRepositoryImpl.class);

    private static final int MAX_SIZE_PER_PAGE = 1000;

    private JpaEntityInformation<T, ID> entityInformation;

    private EntityManager entityManager;

    /**
     * 构造函数
     */
    @SuppressWarnings("unchecked")
    public BaseRepositoryImpl(final JpaEntityInformation<T, ?> entityInformation,
                              EntityManager entityManager) {
        super(entityInformation, entityManager);
        // Keep the EntityManager around to used from the newly introduced methods.
        this.entityManager = entityManager;
        this.entityInformation = (JpaEntityInformation<T, ID>) entityInformation;
    }

    @Override
    @Transactional
    public void saveOrupdate(T entity) throws SaveException {
        try {
            super.save(entity);
        } catch (Exception e) {
            throw new SaveException(e);
        }
    }

    /**
     * 批量添加数据，3.0版本已经与saveOrUpdateEntities合并，两方法实现一致
     */
    @Override
    public void saveEntities(List<T> entities) throws SaveException {
        try {
            super.save(entities);
        } catch (Exception e) {
            throw new SaveException();
        }
    }

    /**
     * 批量添加数据，3.0版本已经与saveEntity合并，两方法实现一致
     */
    @Override
    public void saveOrUpdateEntities(List<T> entities) throws SaveException {
        try {
            super.save(entities);
        } catch (Exception e) {
            throw new SaveException(e);
        }
    }

    @Override
    public int updateByHql(String hql) throws SaveException {
        try {
            return entityManager.createQuery(hql).executeUpdate();
            //session.createQuery(jpql).executeUpdate();
        } catch (Exception e) {
            throw new SaveException(e);
        }
    }

    @Override
    public int updateBySql(String sql) throws Exception {
        return excuteSql(sql, null);
    }

    /*
     * (non-Javadoc)
     * @see com.mrd.base.repository.BaseDao#excetuSql(java.lang.String, java.util.Map)
     */
    @Override
    public int excuteSql(String sql, Map<String, Object> parameters) throws Exception {
        try {
            Query query = entityManager.createNativeQuery(sql);
            if (parameters != null) {
                for (String parameter : parameters.keySet()) {
                    query.setParameter(parameter, parameters.get(parameter));
                }
            }
            return query.executeUpdate();
        } catch (Exception e) {
            throw new SaveException(e);
        }
    }

//    @Override
//    public void delete(T entity) throws DeleteException {
//        delete(entity, false);
//    }

    @Override
    public void delete(T entity, boolean findBeforeDelete) throws DeleteException {
        try {
            if (findBeforeDelete) {
                ID id = entityInformation.getId(entity);
                if (id != null && StringUtils.isNoneEmpty(id.toString())) {
                    entity = super.findOne(id);
                }
            }
            super.delete(entity);
        } catch (Exception e) {
            throw new DeleteException(e);
        }
    }

    /*
     * (non-Javadoc)
     * @see com.mrd.base.BaseDa#findEntityByPage(int, int)
     */
    @Override
    public List<T> findEntityByPage(int startIndex, int size) throws QueryException {
        try {
            PageRequest page = getPage(startIndex, size, null);
            return super.findAll(page).getContent();
        } catch (Exception e) {
            throw new QueryException(e);
        }
    }

    /**
     * 通过jqgrid过滤对象过滤查询结果,当startIndex大于0时分页才会生效
     *
     * @param startIndex 分页起始值
     * @param size       分页大小
     * @param filter     过滤对象
     * @param order      排序对象null为不排序
     */
    public List<T> findEntityByFilter(int startIndex, int size, Filter filter, Sort.Order order)
            throws QueryException {
        if (filter == null) {
            return findEntityByPage(startIndex, size, order);
        }
        Pageable page = getPage(startIndex, size, order);
        Jqgrid jqgrid = new Jqgrid(filter, page);
        try {
            return findByJqgrid(jqgrid).getContent();
        } catch (QueryException e) {
            return null;
        }
    }

    /*
     * (non-Javadoc)
     * @see com.mrd.base.BaseDa#findEntityByPage(java.lang.String, int, int)
     */
    @Override
    @SuppressWarnings("unchecked")
    public List<T> findEntityByPage(final String jpql, int startIndex, int size) throws QueryException {
        try {
            Query query = entityManager.createQuery(jpql);
            if (startIndex > 0) {
                query.setFirstResult(startIndex - 1).setMaxResults(size).getResultList();
            }
            return query.getResultList();
        } catch (DataAccessResourceFailureException e) {
            throw new QueryException(e);
        }
    }

    /*
     * (non-Javadoc)
     * @see com.mrd.base.repository.BaseDao#findEntityByPage(int, int, java.lang.String[])
     */
    @Override
    public List<T> findEntityByPage(int startIndex, int size, Sort.Order order) throws QueryException {
        try {
            Pageable page = getPage(startIndex, size, order);
            return super.findAll(page).getContent();
        } catch (Exception e) {
            throw new QueryException(e);
        }

    }

    /*
     * (non-Javadoc)
     * @see com.mrd.base.BaseDa#findEntityCount()
     */
    @Override
    public int findEntityCount() throws QueryException {
        try {
            long c = super.count();
            return Long.valueOf(c).intValue();
        } catch (Exception e) {
            throw new QueryException(e);
        }
    }

    @Override
    public int findEntityCount(Filter filter) throws QueryException {
        Specification<T> specification = new FilterSpecification(filter);
        return (int) count(specification);
//        if (filter == null) {
//            return findEntityCount();
//        }
//        try {
//            DetachedCriteria criteria = changeFilterToDetachedCriteria(filter);
//            Long numLong = (Long) criteria.getExecutableCriteria(session)
//                    .setProjection(Projections.rowCount()).uniqueResult();
//            String numStr = numLong + "";// 不能直接转成integer类型
//            return Integer.parseInt(numStr);
//        } catch (Exception e) {
//            throw new QueryException(e);
//        }
    }

    /**
     * 根据指定HQL查询该Hql返回的记录数
     *
     * @param jpql 查询记录数的hql
     */
    @Override
    public int findEntityCountByHql(String jpql, Map<String, Object> parameters)
            throws QueryException {
        try {
//            String hqlCount = "select count(*) from (" + jpql + ")";
            Query query = entityManager.createQuery(jpql);
            if (parameters != null && parameters.size() != 0) {
                for (String st : parameters.keySet()) {
                    query.setParameter(st, parameters.get(st));
                }
            }
            List rs = query.getResultList();
            if (rs != null && rs.size() != 0) {
//                BigDecimal os = (   ) rs.get(0);
                Long os = (Long) rs.get(0);
                return os.intValue();
            }
            return 0;
        } catch (Exception e) {
            e.printStackTrace();
            throw new QueryException(e);
        }
    }

    /**
     * 根据指定SQL查询该Sql返回的记录数
     *
     * @param sql 查询数据的sql,不是查询记录数的sql
     */
    @Override
    public int findEntityCountBySql(String sql, Map<String, Object> parameters)
            throws QueryException {
        try {
            String sqlcount = "select count(*) from (" + sql + ")";
            Query query = entityManager.createNativeQuery(sqlcount);
            if (parameters != null && parameters.size() != 0) {
                for (String st : parameters.keySet()) {
                    query.setParameter(st, parameters.get(st));
                }
            }
            List rs = query.getResultList();
            if (rs != null && rs.size() != 0) {
                BigDecimal os = (BigDecimal) rs.get(0);
                return os.intValue();
            }
            return 0;
        } catch (Exception e) {
            throw new QueryException(e);
        }
    }

    /*
     * (non-Javadoc)
     * @see com.mrd.base.BaseDao#findEntityById(java.lang.String)
     */
    @Override
    public T findEntityById(ID id) throws QueryException {
        try {
            return super.findOne(id);
        } catch (Exception e) {
            throw new QueryException(e);
        }
    }

    @Override
    public List<T> findAllEntity() throws QueryException {
        try {
            return findAll();
        } catch (Exception e) {
            throw new QueryException(e);
        }
    }

    /*
     * (non-Javadoc)
     * @see com.mrd.base.repository.BaseDao#delete(java.lang.String, java.lang.Object)
     */
    @Override
    public int delete(String column, Object value) throws DeleteException {
        try {
            StringBuilder jpql = new StringBuilder();
            jpql.append("delete from ").append(entityInformation.getEntityName()).append(" where ")
                    .append(column).append("=:").append(column);
            return entityManager.createQuery(jpql.toString()).setParameter(column, value).executeUpdate();
        } catch (Exception e) {
            throw new DeleteException(e);
        }
    }

    /*
     * (non-Javadoc)
     * @see com.mrd.base.repository.BaseDao#delete(java.lang.String, java.util.Map)
     */
    @Override
    public int delete(String jpql, Map<String, Object> values) throws DeleteException {
        try {
            Query query = entityManager.createQuery(jpql);
            if (values != null) {
                for (String paraName : values.keySet()) {
                    query.setParameter(paraName, values);
                }
            }
            return query.executeUpdate();
        } catch (Exception e) {
            throw new DeleteException(e);
        }
    }

    /*
     * (non-Javadoc)
     * @see com.mrd.base.repository.BaseDao#findEntityByFilter(java.lang.String, int, int, java.util.Map)
     */
    @Override
    @SuppressWarnings("unchecked")
    public List<Object> findEntityByHql(String jpql, int page, int size,
                                        Map<String, Object> parameters) throws QueryException {
        try {
            // 因为hibernate查询时不包含起始那条数据所以减1
            Query query = entityManager.createQuery(jpql);
            if (parameters != null) {
                for (String parameter : parameters.keySet()) {
                    query.setParameter(parameter, parameters.get(parameter));
                }
            }
            if (page > 0) {
                query.setFirstResult((page - 1) * size).setMaxResults(size);
            }
            return query.getResultList();
        } catch (DataAccessResourceFailureException e) {
            throw new QueryException(e);
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<Object> findEntityBySql(String sql, int startIndex, int size,
                                        Map<String, Object> parameters) throws QueryException {
        try {
            // 因为hibernate查询时不包含起始那条数据所以减1
            Query query = entityManager.createNativeQuery(sql);
            if (parameters != null) {
                for (String parameter : parameters.keySet()) {
                    query.setParameter(parameter, parameters.get(parameter));
                }
            }
            if (startIndex > 0) {
                query.setFirstResult(startIndex - 1).setMaxResults(size);
            }
            return query.getResultList();
        } catch (DataAccessResourceFailureException e) {
            throw new QueryException(e);
        }
    }

//    @Override
//    public List<T> findEntityByHql(final String hql, int startIndex, int size, Filter filters) {
//        try {
//            // Criterion c = changeFilterToCiriter(filters);
//            // return findEntityByPage(jpql + " and " + c.toString(), startIndex, size);
//            return findEntityByPage(hql, startIndex, size);
//        } catch (QueryException e) {
//            e.printStackTrace();
//        }
//        return null;
//    }

    public Page<T> findByJqgrid(Jqgrid jqgrid) throws QueryException {
        return findByJqgrid(jqgrid, null);
    }

    public Page<T> findByJqgrid(Jqgrid jqgrid, String graphName) throws QueryException {
        Specification<T> specification = new FilterSpecification(jqgrid.getFilters());
        TypedQuery<T> typedQuery = getQuery(specification, jqgrid.getPage());
        applyGraph(typedQuery, graphName);
        return readPage(typedQuery, jqgrid.getPage(), specification);
    }

    public List<T> findByFilter(Filter filter) throws QueryException {
        return findByFilter(filter, null);
    }

    public List<T> findByFilter(Filter filter, String graphName) throws QueryException {
        Specification<T> specification = new FilterSpecification(filter);
        TypedQuery<T> typedQuery = getQuery(specification, (Sort) null);
        applyGraph(typedQuery, graphName);
        return typedQuery.getResultList();
    }

    private void applyGraph(TypedQuery<T> typedQuery, String graphName) {
        if (StringUtils.isNotEmpty(graphName)) {
            JpaEntityGraph entityGraph = new JpaEntityGraph(graphName, EntityGraph.EntityGraphType.LOAD, null);
            Map<String, Object> hints = Jpa21Utils.tryGetFetchGraphHints(entityManager, entityGraph, getDomainClass());
            for (String s : hints.keySet()) {
                typedQuery.setHint(s, hints.get(s));
            }
        }
    }

    class FilterSpecification implements Specification<T> {

        private Filter filter;

        public FilterSpecification(Filter filter) {
            super();
            this.filter = filter;
        }

        @Override
        public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            if (filter == null) {
                return null;
            }
            if (filter.getRules() == null || filter.getRules().isEmpty()) {
                return null;
            }
            List<Predicate> predicates = new ArrayList<Predicate>();
            for (SearchRule rule : filter.getRules()) {
                Predicate p = null;
                Object data = rule.getData();
                String fieldName = rule.getField();
                if (data == null && !(rule.getOp().equals(SearchRuleOption.inl) && rule.getOp().equals(SearchRuleOption.nnl))) {
                    continue;
                }
                switch (rule.getOp()) {
                    case eq:
                        Expression expression = getPath(root, fieldName);
//                        Class clazz = expression.getJavaType();
//                        if (java.lang.Boolean.class.equals(clazz)) {
//                            data = (data == null || (Boolean) data) ? "0" : "1";
//                        }
                        p = cb.equal(expression, data);
                        break;
                    case ne:
                        p = cb.notEqual(getPath(root, fieldName), data);
                        break;
                    case cn:
                        p = cb.like(getPath(root, fieldName).as(String.class), "%"
                                + data + "%");
                        break;
                    case nc:
                        p = cb.notLike(getPath(root, fieldName).as(String.class), "%"
                                + data + "%");
                        break;
                    case bw:
                        p = cb.like(getPath(root, fieldName).as(String.class), "%"
                                + data);
                        break;
                    case bn:
                        p = cb.notLike(getPath(root, fieldName).as(String.class), "%"
                                + data);
                        break;
                    case ew:
                        p = cb.like(getPath(root, fieldName).as(String.class), data
                                + "%");
                        break;
                    case en:
                        p = cb.notLike(getPath(root, fieldName).as(String.class), data + "%");
                        break;
                    case inl:
                        p = cb.isNull(getPath(root, fieldName));
                        break;
                    case nnl:
                        p = cb.isNotNull(getPath(root, fieldName));
                        break;
                    case btay:
                        logger.warn("暂未实现{}年度查询操作方法", rule.getOp());
                        break;
                    case btam:
                        logger.warn("暂未实现{}月度查询操作方法", rule.getOp());
                        break;
                    case gt:
                        data = dateCover(data);
                        if (data instanceof Date) {
                            p = cb.greaterThan(getPath(root, fieldName).as(Date.class), (Date) data);
                        } else {
                            p = cb.greaterThan(getPath(root, fieldName).as(String.class), String.valueOf(data));
                        }
                        break;
                    case ge:
                        data = dateCover(data);
                        if (data instanceof Date) {
                            p = cb.greaterThanOrEqualTo(getPath(root, fieldName).as(Date.class), (Date) data);
                        } else {
                            p = cb.greaterThanOrEqualTo(getPath(root, fieldName).as(String.class), String.valueOf(data));
                        }
                        break;
                    case le:
                        data = dateCover(data);
                        if (data instanceof Date) {
                            Date _data = (Date) data;
                            _data.setHours(23);
                            _data.setMinutes(59);
                            _data.setSeconds(59);
                            p = cb.lessThanOrEqualTo(getPath(root, fieldName).as(Date.class), _data);
                        } else {
                            p = cb.lessThanOrEqualTo(getPath(root, fieldName).as(String.class), String.valueOf(data));
                        }
                        break;
                    case lt:
                        data = dateCover(data);
                        if (data instanceof Date) {
                            Date _data = (Date) data;
                            _data.setHours(23);
                            _data.setMinutes(59);
                            _data.setSeconds(59);
                            p = cb.lessThan(getPath(root, fieldName).as(Date.class), _data);
                        } else {
                            p = cb.lessThan(getPath(root, fieldName).as(String.class), String.valueOf(data));
                        }
                        break;
                    case in:
                        p = getPath(root, fieldName).in(data);
                        break;
                    case ni:
                        p = getPath(root, fieldName).in(data).not();
                        break;
                    case ex_like:
                        logger.warn("暂未实现{}操作方法", rule.getOp());
                        break;
                    case ex_in:
                        logger.warn("暂未实现{}操作方法", rule.getOp());
                        break;
                    default:
                        logger.warn("暂未实现{}操作方法", rule.getOp());
                        break;
                }
                if (p != null) {
                    predicates.add(p);
                }
            }
            if (filter.getGroups() != null) {
                predicates.add(new FilterSpecification(filter.getGroups())
                        .toPredicate(root, query, cb));
            }
            switch (filter.getGroupOp()) {
                case AND:
                    return cb.and(predicates.toArray(new Predicate[0]));
                case OR:
                    return cb.or(predicates.toArray(new Predicate[0]));
                default:
                    return null;
            }
        }

        private Expression<?> getPath(Root<T> root, String field) {
            if (!field.contains(".")) {
                return root.get(field);
            } else {
                String[] fields = field.split("\\.");
                Path<?> path = root.get(fields[0]);
                for (int i = 1; i < fields.length; i++) {
                    path = path.get(fields[i]);
                }
                return path;
            }
        }

        /**
         * 识别数据是否可转换成日期数据，如果可以转换则转换，不可以则返回原数据
         *
         * @param data 待识别数据
         * @return 经过转换的数据或元数据
         */
        private Object dateCover(Object data) {
            Object newData = data;
            if (data != null && String.class.equals(data.getClass())) {
                try {
                    newData = DateUtils.parseDate((String) data, datePatterns);
                } catch (ParseException e) {
                    logger.warn("不能将字符串[{}]解析为日期", data);
                    newData = data;
                }
            }
            return newData;
        }
    }

    /**
     * 可能存在的日期格式
     */
    private static final String[] datePatterns = {"yyyyMMdd", "yyyy-MM-dd", "yyyyMMdd HH:mm:ss"};

    /*
     * (non-Javadoc)
     * @see com.mrd.base.repository.BaseDao#oprateData(com.mrd.util.search.Filter)
     */
    @Override
    public int deleteByFilter(Filter filter) throws DeleteException {
        // TODO: 2016/8/13
//        int i = 0;
//        try (Session session = openSession()) {
//            Criterion criterion = changeFilterToCiriter(filter);
//            String conditions = criterion.toString();
//            String[] c = conditions.split("=");
//            StringBuffer sqlConditions = new StringBuffer();
//            String[] sqlParemetersValues = null;
//            if (c.length > 1) {
//                sqlParemetersValues = new String[c.length - 1];
//                for (int j = 0; j < c.length; j++) {
//                    if (j == 0) {
//                        sqlConditions.append(c[j]);
//                    } else {
//                        sqlParemetersValues[j - 1] = c[j].substring(0, c[j].indexOf(")"));
//                        sqlConditions.append("=?").append(c[j].substring(c[j].indexOf(")")));
//                    }
//                }
//                if (c.length == 2) {
//                    sqlConditions.deleteCharAt(0);
//                    sqlConditions.deleteCharAt(sqlConditions.length() - 1);
//                }
//            } else {
//                sqlConditions.append(conditions);
//            }
//            StringBuffer sql = new StringBuffer();
//            sql.append("delete from ").append(getEntityClass().getName()).append(" where ")
//                    .append(sqlConditions);
//            Query query = session.createQuery(sql.toString());
//            if (null != sqlParemetersValues) {
//                for (int j = 0; j < sqlParemetersValues.length; j++) {
//                    query.setParameter(j, sqlParemetersValues[j]);
//                }
//            }
//            i = query.executeUpdate();
//        } catch (Exception e) {
//            e.printStackTrace();
//            throw new DeleteException(e);
//        }
//        return i;
        return 0;
    }

    @Override
    public List<?> findListByHql(String queryHql, Object[] params) throws QueryException {
        Query query = entityManager.createQuery(queryHql);
        for (int i = 0; i < params.length; i++) {
            query.setParameter(i, params[i]);
        }
        return query.getResultList();
    }

    /**
     * 创建分页
     *
     * @param startIndex 开始值
     * @param size       分页大小
     * @return 分页对象
     */
    private PageRequest getPage(int startIndex, int size) {
        return getPage(startIndex, size, null);
    }

    /**
     * 创建分页
     *
     * @param startIndex 开始值
     * @param size       分页大小
     * @param order      排序
     * @return 分页对象
     */
    private PageRequest getPage(int startIndex, int size, Sort.Order order) {
        int page = 0;
        if (startIndex > 1) {
            page = (startIndex - 1) / size;
        }
        if (size < 1) {
            size = MAX_SIZE_PER_PAGE;
        }
        PageRequest pageRequest;
        if (order == null) {
            pageRequest = new PageRequest(page, size);
        } else {
            Sort sort = new Sort(order);
            pageRequest = new PageRequest(page, size, sort);
        }
        return pageRequest;
    }
}
